//
//  MujiMediaLoadRequest.m
//  ECCoreLibrary
//
//  Created by Paul Cantrell on 6/11/14.
//  Copyright (c) 2014 EnglishCentral, Inc. All rights reserved.
//

#import "MujiMediaLoadRequest.h"
#import "MujiMediaLoadRequestManager.h"
#import "MujiMediaViewAssetResourceLoaderDelegate.h"
//#import "ECCoreLibraryDataModelConstants.h"
//#import "Cocoalumberjack.h"

#import <unistd.h>
#import <fcntl.h>

static NSString * const kMediaPlayableKey       = @"playable";
static NSString * const kMediaTracksKey         = @"tracks";
static NSString * const kMediaDuration          = @"duration";

//#ifdef DEBUG
//static DDLogLevel ddLogLevel = DDLogLevelWarning;
//#else
//static DDLogLevel ddLogLevel = DDLogLevelError;
//#endif

float const EC3GTimeoutInterval         = 10.0f;
@interface MujiMediaLoadRequest ()

@property (nonatomic, copy) NSString *originalUrlString;
@property (nonatomic, copy) NSString *encodedUrlString;
@property (nonatomic, copy) NSString *localFilename;
@property (nonatomic, copy) NSDate *createDate;
@property (nonatomic, copy) dispatch_queue_t delegateAccessQueue;
@property (nonatomic, strong) NSArray *delegates;
@property (nonatomic,strong) NSURLConnection *connection;
@property (nonatomic,strong) NSString *mimeType;
@property (nonatomic,assign) int fileDescriptor;
@property (nonatomic,assign) UInt64 fileHighwaterMark;
@property (nonatomic,strong) MujiMediaViewAssetResourceLoaderDelegate *loaderDelegate;
@property (nonatomic,weak) id<MujiMediaLoadRequestProtocol> clientDelegate;

@end

@implementation MujiMediaLoadRequest

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        _delegateAccessQueue = dispatch_queue_create("MediaLoadRequestDelegates", DISPATCH_QUEUE_SERIAL);
        _createDate          = [[NSDate date] copy];
        
        _clientAccessQueue = dispatch_queue_create("MediaLoadRequest", DISPATCH_QUEUE_SERIAL);
        //        _serialLoaderQueue = dispatch_queue_create("AssetLoaderSerialQueue", DISPATCH_QUEUE_SERIAL);
    }
    
    return self;
}



// This method is called by the MediaLoadRequestManager when it needs to make a new load request.

- (instancetype)initWithUrlString:(NSString *)urlString
{
    self = [self init];
    if (self)
    {
        _originalUrlString  = [urlString copy];
        CFUUIDRef uuid      = CFUUIDCreate(NULL);
        NSString *guid      = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuid);
        NSString *extension = [urlString pathExtension];
        _localFilename      = [[NSString stringWithFormat:@"CachedMovie%@.%@",guid,extension] copy];
        CFRelease(uuid);
        
        NSDictionary *optionsDict = @{AVURLAssetPreferPreciseDurationAndTimingKey : @NO,
                                      AVURLAssetReferenceRestrictionsKey : [NSNumber numberWithInt:AVAssetReferenceRestrictionForbidNone]};
        NSURL *aURL = [NSURL URLWithString:self.encodedUrlString];
        _avAsset = [AVURLAsset URLAssetWithURL:aURL options:optionsDict];
        _loaderDelegate = [[MujiMediaViewAssetResourceLoaderDelegate alloc] initWithMediaLoadRequest:self];
        AVAssetResourceLoader *assetLoader = _avAsset.resourceLoader;
        [assetLoader setDelegate:_loaderDelegate queue:_clientAccessQueue];
        [self startDownload];
    }
    return self;
}

// This method is used to decode a serialized representation of a load request object.

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [self init];
    if (self)
    {
        self.originalUrlString   = [[aDecoder decodeObjectForKey:@"originalUrlString"] copy];
        self.encodedUrlString    = [[aDecoder decodeObjectForKey:@"encodedUrlString"] copy];
        self.localFilename       = [[aDecoder decodeObjectForKey:@"localFilename"] copy];
        self.contentLength       = [aDecoder decodeInt64ForKey:@"contentLength"];
        self.downloadHasFinished = [aDecoder decodeBoolForKey:@"downloadHasFinished"];
        self.createDate          = [[aDecoder decodeObjectForKey:@"createDate"] copy];
        self.downloadInProgress  = [aDecoder decodeBoolForKey:NSStringFromSelector(@selector(isDownloadInProgress))];
    }
    
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.originalUrlString forKey:@"originalUrlString"];
    [aCoder encodeObject:self.encodedUrlString forKey:@"encodedUrlString"];
    [aCoder encodeObject:self.localFilename forKey:@"localFilename"];
    [aCoder encodeInt64:self.contentLength forKey:@"contentLength"];
    [aCoder encodeBool:self.downloadHasFinished forKey:@"downloadHasFinished"];
    [aCoder encodeObject:self.createDate forKey:@"createDate"];
    [aCoder encodeBool:self.downloadInProgress forKey:NSStringFromSelector(@selector(isDownloadInProgress))];
}


#pragma mark -
#pragma mark NSObject

- (void)dealloc
{
    // This shouldn't happen, but just in case, be sure we don't leak file descriptors
    if( _fileDescriptor > 0 )
    {
        close(_fileDescriptor);
    }
}


#pragma mark - Movie File Handling Methods

- (void)closeMovieFile
{
    //DDLogInfo(@"%@ :: Closing movie file", NSStringFromClass([self class]));
    if( _fileDescriptor >= 0 )
    {
        close(_fileDescriptor);
        _fileDescriptor = -1;
    }
}


- (void)loadAssetWithDelegate:(id<MujiMediaLoadRequestProtocol>)delegate
{
    //DDLogInfo(@"%@ :: Loading asset with delegate", NSStringFromClass([self class]));
    self.clientDelegate    = delegate;
    NSArray *requestedKeys = @[kMediaPlayableKey,kMediaTracksKey,kMediaDuration];
    
    MujiMediaLoadRequest * __weak weakSelf = self;
    //DDLogInfo(@"%@ :: Loading values asynchronously", NSStringFromClass([self class]));
    [self.avAsset loadValuesAsynchronouslyForKeys:requestedKeys
                                completionHandler: ^
     {
         MujiMediaLoadRequest * strongSelf = weakSelf;
         [strongSelf prepareToPlayAsset:strongSelf.avAsset withKeys:requestedKeys];
     }];
}

- (void)prepareToPlayAsset: (AVURLAsset *)asset withKeys:(NSArray *)requestedKeys
{
    //DDLogInfo(@"%@ :: Preparing the play asset", NSStringFromClass([self class]));
    if ([self.clientDelegate respondsToSelector:@selector(prepareToPlayAsset:withKeys:)])
    {
        [self.clientDelegate prepareToPlayAsset:asset withKeys:requestedKeys];
    }
}

- (void)setDate:(NSDate *)date
{
    _createDate = [date copy];
}

// Returns the encoded path for a load. The reason we need this is that we need to trick the
// media player to load from our source, which is our built in web server under iOS6 or our
// load delegate under iOS7

- (NSString *)encodedUrlString
{
    if (!_encodedUrlString)
    {
        //DDLogInfo(@"%@ :: Creating encoded url string", NSStringFromClass([self class]));
        NSURL *url                = [NSURL URLWithString:_originalUrlString];
        NSString *absoluteURLPath = [url absoluteString];
        _encodedUrlString         = [[absoluteURLPath stringByReplacingOccurrencesOfString:@"http://" withString:@"httpxxx://"] copy];
        NSLog(@"Media Load Request Created Encoded URL String: %@", _encodedUrlString);
    }
    
    return _encodedUrlString;
}

- (NSString *)originalUrlString
{
    return _originalUrlString;
}

- (NSString *)localFilename
{
    return _localFilename;
}

// Public method to cancel any download which may be taking place, and return an indication about
// whether a download was actually in progress. This is used by the MediaLoadRequestManager to
// determine whether it has a partial download that needs to be cleaned up, or a completed download
// which should be preserved. Also, this insures we don't leak unix file descriptors. Implemented
// in the child class...

- (BOOL)cancelDownload
{
    //DDLogInfo(@"%@ :: Cancelling download", NSStringFromClass([self class]));
    if(self.fileDescriptor > 0)
    {
        //DDLogInfo(@"%@ :: Will close file descriptor", NSStringFromClass([self class]));
        if (!close(self.fileDescriptor))
        {
//            DDLogWarn(@"%@ :: Failed to close file descriptor:%ld",
//                      NSStringFromClass([self class]),
//                      (long)self.fileDescriptor);
        }
        
        self.fileDescriptor = 0;
    }
    
    if(self.connection)
    {
        //DDLogInfo(@"%@ :: Cancelling connection", NSStringFromClass([self class]));
        [self.connection cancel];
        
        if(!self.downloadHasFinished )
        {
            //DDLogInfo(@"%@ :: Cancelled connection", NSStringFromClass([self class]));
            return YES;
        }
    }
    
    //DDLogInfo(@"%@ :: Connection was already cancelled", NSStringFromClass([self class]));
    return NO;
}

// Public method called by MediaLoadRequestManager to test whether the cached movie file is present
// and of the correct size. If it's the wrong size it might indicate an interrupted download that
// somehow got marked as completed, even though it didn't all get written to disk. It's a
// conservative way to avoid caching a corrupted movie file.

- (BOOL)localFileIsPresent
{
    BOOL isPresent = NO;
    @try
    {
        NSString *filename     = [self localFilePath];
        NSFileManager *manager = [NSFileManager defaultManager];
        if([manager fileExistsAtPath:filename] &&
           [manager isReadableFileAtPath:filename])
        {
            NSError *error           = nil;
            NSDictionary *attributes = [manager attributesOfItemAtPath:filename error:&error];
            NSNumber *fileSizeNum    = [attributes objectForKey:NSFileSize];
            UInt64 fileSize          = fileSizeNum.longLongValue;
            if(fileSize == _contentLength)
            {
                isPresent = YES;
            }
        }
    }
    @catch (NSException *exception)
    {
        isPresent = NO;
        //DDLogError(@"%@ :: Caught exception checking if local file is present:%@", NSStringFromClass([self class]), exception);
    }
    @finally
    {
        return isPresent;
    }
}

// Public method to delete the associated mpvie file

- (void)uncache
{
    NSString *moviePath = [self localFilePath];
    NSError *error;
    [[NSFileManager defaultManager] removeItemAtPath:moviePath error:&error];
}

#pragma mark - Delegate Logic

// Unlike the typical delegate pattern, we support more than one delegate because the
// MediaLoadRequestManager will be a delegate as well as the media player download
// delegate. Note that this means our delegates array is holding a strong reference
// to the delegates.

- (void)addAsDelegate:(id<MujiMediaLoadRequestDelegate>)delegate
{
    if (self.delegateAccessQueue)
    {
        MujiMediaLoadRequest * __weak weakSelf = self;
        dispatch_sync(self.delegateAccessQueue,^
                      {
                          MujiMediaLoadRequest * strongSelf = weakSelf;
                          NSMutableArray *mDelegates      = strongSelf.delegates ? [strongSelf.delegates mutableCopy] : [@[] mutableCopy];
                          
                          @try
                          {
                              [mDelegates addObject:delegate];
                          }
                          @catch (NSException * exception)
                          {
//                              DDLogError(@"%@ :: Caught exception in delegate access queue when adding new delegate to mutable delegate collection -- %@",
//                                         NSStringFromClass([strongSelf class]),
//                                         exception);
                          }
                          @finally
                          {
                              strongSelf.delegates = [NSArray arrayWithArray:mDelegates];
                          }
                      });
    }
}

- (void)removeAsDelegate:(id<MujiMediaLoadRequestDelegate>)delegate
{
    if (self.delegateAccessQueue)
    {
        MujiMediaLoadRequest * __weak weakSelf = self;
        dispatch_sync(self.delegateAccessQueue, ^
                      {
                          MujiMediaLoadRequest * strongSelf = weakSelf;
                          NSMutableArray *mDelegates = strongSelf.delegates ? [strongSelf.delegates mutableCopy] : [@[] mutableCopy];
                          @try
                          {
                              [mDelegates removeObjectIdenticalTo:delegate];
                          }
                          @catch (NSException * exception)
                          {
//                              DDLogError(@"%@ :: Caught exception in delegate access queue trying to remove delegate from delegates collection -- %@",
//                                         NSStringFromClass([strongSelf class]),
//                                         exception);
                          }
                          @finally
                          {
                              strongSelf.delegates = [NSArray arrayWithArray:mDelegates];
                          }
                      });
    }
}

- (NSArray *)delegates
{
    return _delegates;
}

#pragma mark - Data Access logic

- (UInt64)availableContentLength
{
    return _fileHighwaterMark;
}

#pragma mark - Request Startup

// This is the logic which starts up the download of the original material

- (void)startDownload
{
    if(!_connection)
    {
        [self createLocalMovieFile];
        NSURL *fixedURL = [NSURL URLWithString:self.originalUrlString];
        NSURLRequest *request = [[NSURLRequest alloc] initWithURL:fixedURL
                                                      cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                  timeoutInterval:EC3GTimeoutInterval];
        _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
        [_connection setDelegateQueue:[NSOperationQueue mainQueue]];
        [_connection start];
    }
}

- (NSString *)localFilePath
{
    NSString *localMoviePath = nil;
    @try
    {
        NSArray *paths       = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        NSString *baseDir    = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
        NSString *moviesPath = [baseDir stringByAppendingPathComponent:@"MoviesCache"];
        localMoviePath       = [moviesPath stringByAppendingPathComponent:_localFilename];
    }
    @catch (NSException * exception)
    {
        //DDLogError(@"%@ :: Caught exception getting the local file path:%@", NSStringFromClass([self class]), exception);
    }
    @finally
    {
        return localMoviePath;
    }
}

- (BOOL)createLocalMovieFile
{
    //DDLogInfo(@"%@ :: Creating local movie file", NSStringFromClass([self class]));
    NSString *moviePath = [self localFilePath];
//    DDLogInfo(@"%@ :: Movie file local file path:%@",
//              NSStringFromClass([self class]),
//              moviePath ?: @"<nil>");
    const char *filename = [moviePath UTF8String];
    mode_t mode = S_IRUSR | S_IWUSR;
    
    //DDLogInfo(@"%@ :: Opening file descriptor", NSStringFromClass([self class]));
    self.fileDescriptor = open(filename, O_RDWR|O_CREAT, mode);
    
    BOOL created = self.fileDescriptor < 0 ? NO : YES;
//    DDLogInfo(@"%@ :: %@",
//              NSStringFromClass([self class]),
//              created ? @"Did create local movie file and opened file descriptor" : @"Failed to open file descriptor for file movie path");
    return created  ;
}

// This is the delegate callback logic

- (void)signalDownloadHasBegun
{
    if (_delegateAccessQueue)
    {
        MujiMediaLoadRequest * __weak weakSelf = self;
        dispatch_sync(_delegateAccessQueue, ^(void)
                      {
                          MujiMediaLoadRequest * strongSelf = weakSelf;
                          strongSelf.downloadInProgress   = YES;
                          for(id<MujiMediaLoadRequestDelegate>delegate in strongSelf.delegates)
                          {
                              [delegate downloadHasBegun];
                          }
                      });
    }
}

- (void)signalNewDataHasArrived
{
    if (_delegateAccessQueue)
    {
        MujiMediaLoadRequest * __weak weakSelf = self;
        dispatch_sync(_delegateAccessQueue, ^(void)
                      {
                          MujiMediaLoadRequest * strongSelf = weakSelf;
                          strongSelf.downloadInProgress = YES;
                          for( id<MujiMediaLoadRequestDelegate>delegate in strongSelf.delegates)
                          {
                              [delegate newDataHasArrived];
                          }
                      });
    }
}

- (void)signalDownloadHasFailedWithError:(NSError *)error
{
    if (self.delegateAccessQueue)
    {
        MujiMediaLoadRequest * __weak weakSelf = self;
        dispatch_sync(self.delegateAccessQueue, ^(void)
                      {
                          MujiMediaLoadRequest * strongSelf = weakSelf;
                          strongSelf.downloadInProgress = NO;
                          for( id<MujiMediaLoadRequestDelegate>delegate in strongSelf.delegates)
                          {
                              [delegate downloadFailed:strongSelf withError:error];
                          }
                      });
    }
}

- (void)signalDownloadHasCompleted
{
//    DDLogInfo(@"%@ :: Signaling that download has completed and will set download in progress:%@ -- To -- NO",
//              NSStringFromClass([self class]),
//              self.downloadInProgress ? @"YES" : @"NO");
    self.downloadInProgress = NO;
}


#pragma mark - NSURLConnection Callbacks

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
//    DDLogInfo(@"%@ :: Received response:\n\t -- expected content length: %lld\n\t -- suggested file name:%@\n\t -- MIME Type:%@\n\t -- Text Encoding Name:%@\n\t -- URL: %@",
//              NSStringFromClass([self class]),
//              (long long)response.expectedContentLength,
//              response.suggestedFilename ?:@"<nil>",
//              response.MIMEType ?: @"<nil>",
//              response.textEncodingName ?: @"<nil>",
//              response.URL ? [response.URL absoluteString] : @"<nil>");
    
    self.response = (NSHTTPURLResponse *)response;
    NSDictionary *headers = [self.response allHeaderFields];
    
    //DDLogInfo(@"%@ :: Response Headers: %@", NSStringFromClass([self class]), headers);
    NSString *contentLengthString = [headers objectForKey:@"Content-Length"];
    if([contentLengthString length])
    {
        self.contentLength = [contentLengthString integerValue];
    }
    
    _mimeType = self.response.MIMEType;
    
    //DDLogInfo(@"%@ :: Signaling that download has begun", NSStringFromClass([self class]));
    [self signalDownloadHasBegun];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    //DDLogInfo(@"%@ :: Connection received data", NSStringFromClass([self class]));
    dispatch_sync(self.clientAccessQueue, ^(void)
    {
        if(self.fileDescriptor > 0)
        {
            const void *bytes = [data bytes];
            size_t length = [data length];
            //DDLogInfo(@"%@ :: Received data has length: %zd", NSStringFromClass([self class]), length);
            size_t completedLength = 0;
            completedLength = write(self.fileDescriptor, bytes, length);
            //DDLogInfo(@"%@ :: Wrote total:%zd to file", NSStringFromClass([self class]), completedLength);
            if( completedLength != length )
            {
                //DDLogInfo(@"%@ :: Closing the movie file since we didn't write the total availbale length", NSStringFromClass([self class]));
                [self closeMovieFile];
            }
            else
            {
                UInt64 currentFileHighWaterMark = self.fileHighwaterMark;
                self.fileHighwaterMark += completedLength;
//                DDLogInfo(@"%@ :: Updating the file highwater mark From:%llu To:%llu",
//                          NSStringFromClass([self class]),
//                          currentFileHighWaterMark,
//                          self.fileHighwaterMark);
            }
        }
    });
    
    //DDLogInfo(@"%@ :: Signaling that new data has arrived from connection received data", NSStringFromClass([self class]));
    [self signalNewDataHasArrived];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    //DDLogInfo(@"%@ :: Finished downloading video with connection:%@", NSStringFromClass([self class]), connection);
    self.downloadHasFinished = YES;
    self.downloadInProgress  = NO;
    
    //DDLogInfo(@"%@ :: Closing movile file", NSStringFromClass([self class]));
    [self closeMovieFile];
    
    //DDLogInfo(@"%@ :: Alerting delegates that download has completed", NSStringFromClass([self class]));
    for( id<MujiMediaLoadRequestDelegate>delegate in [self delegates] )
    {
        [delegate downloadHasCompleted];
    }
    
    if( self.mediaClientDelegate &&
       [self.mediaClientDelegate respondsToSelector:@selector(downloadCompleted:)])
    {
//        DDLogInfo(@"%@ :: Signaling media client delegate that download has completed on main thread.  Will not wait",
//                  NSStringFromClass([self class]));
        NSObject *delegate = (NSObject *)self.mediaClientDelegate;
        [delegate performSelectorOnMainThread:@selector(downloadCompleted:)
                                   withObject:self
                                waitUntilDone:NO];
    }
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
//    DDLogError(@"%@ :: Connection failed with error:%@",
//               NSStringFromClass([self class]),
//               error);
    self.downloadHasFinished = YES;
    self.downloadInProgress  = NO;
    
    //DDLogInfo(@"%@ :: Closing movie file", NSStringFromClass([self class]));
    [self closeMovieFile];
    
    //DDLogInfo(@"%@ :: Alerting delegates that download has failed", NSStringFromClass([self class]));
    for( id<MujiMediaLoadRequestDelegate>delegate in [self delegates] )
    {
        [delegate downloadFailed:self withError:error];
    }
    
    if( self.mediaClientDelegate &&
       [self.mediaClientDelegate respondsToSelector:@selector(downloadFailed:)])
    {
        //DDLogInfo(@"%@ :: Alerting media client delegate that download has failed", NSStringFromClass([self class]));
        NSObject *delegate = (NSObject *)self.mediaClientDelegate;
        [delegate performSelectorOnMainThread:@selector(downloadFailed:)
                                   withObject:self
                                waitUntilDone:NO];
    }
}




@end
